﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Red.Core.DataConsistency
{
    /// <summary>
    /// State synchronizer - global focal point for state synchronization between different parts of the data
    /// This is not a singleton but is usually owned by something rather high in the object hierarchy
    /// This class allows you to create "state groups" that can get notified (via registered delegate) that
    /// any of the ConsistentObject's tracked within that group (via StateTokens) have changed.
    /// NOTE: since there are usually A LOT of single events modifying objects from the same group related to each other 
    /// that are created in a very short period of time servicing every single one of them would NOT be a good idea.
    /// State synchronizer is collecting (via OR mask) all the notification for registered groups and the final even is dispatched only once in a given time frame.
    /// NOTE: state synchronizer requires a direct call to DispatchPendingEvents() in order to execute the callbacks.
    /// This can be either done in timer, via thread or manually.
    /// </summary>
    public partial class StateSynchronizer
    {
        /// <summary>
        /// Maximum number of tracked groups in the system, that's really really generous
        /// </summary>
        private static UInt32 MAX_GROUPS = 65536;

        /// The global instance
        public static readonly StateSynchronizer TheInstance = new StateSynchronizer();

        /// <summary>
        /// Initialize
        /// </summary>
        private StateSynchronizer()
        {
            // prepare the dirty mask
            _GroupDirtyMask = new UInt64[ MAX_GROUPS / 64 ];
            _Groups = new Dictionary<UInt32, StateGroup>();
            _GroupsMaskDirty = false;

            // generations
            _GroupGenerationIDCounter = 42;
            _GroupGenerationIDs = new UInt32[MAX_GROUPS];

            // free indices
            _FreeGroupIndices = new List<UInt32>();
            _FirstFreeGroupIndex = 1;

            // access lock
            _Lock = new object();
        }

        /// <summary>
        /// Create state group
        /// </summary>
        /// <param name="debugName">Debug name</param>
        /// <returns>Created state group</returns>
        public StateGroup CreateGroup(string debugName)
        {
            lock (_Lock)
            {
                // allocate the group ID
                var id = AllocGroupID_NoLock();
                Debug.Assert( IsGroupMarked_NoLock(id) == false );

                // create the wrapper
                var generationId = AllocGroupGeneraion_NoLock();
                var group = new StateGroup(debugName, id, generationId, this.UnregisterGroup);

                // register in the global map
                _Groups[id] = group;
                return group;
            }
        }

        /// <summary>
        /// (internal) Unregister group from local store
        /// </summary>
        /// <param name="group">Group to ungregister</param>
        private void UnregisterGroup(StateGroup group)
        {
            lock (_Lock)
            {
                if (_Groups.ContainsValue(group))
                {
                    _Groups.Remove(group.GroupID);
                    UnmarkGroup_NoLock(group.GroupID);
                }
            }
        }

        /// <summary>
        /// Fire cached events for modified state groups
        /// </summary>
        public void DispatchPendingEvents()
        {
            // extract modified groups, the only locking part
            var modifiedGroups = new List<StateGroup>();
            lock (_Lock)
            {
                if (_GroupsMaskDirty)
                {                    
                    foreach (var group in _Groups)
                    {
                        if (IsGroupMarked_NoLock(group.Key))
                        {
                            UnmarkGroup_NoLock(group.Key);
                            modifiedGroups.Add(group.Value);
                        }
                    }

                    _GroupsMaskDirty = false;
                }                    
            }

            // process the group events
            foreach (var group in modifiedGroups)
                group.DispatchEvent();
        }

        /// <summary>
        /// Mark group as changed
        /// </summary>
        /// <param name="groupID">Group ID of the group for which the flag should be set</param>
        public void MarkGroup(UInt32 groupID, UInt32 groupGeneration)
        {
            lock (_Lock)
            {
                UInt64 index = (UInt64)groupID / 64;
                UInt64 mask = (UInt64)groupID & 63;

                if ((_GroupDirtyMask[index] & mask) == 0)
                {
                    // only mark actual group
                    if (_GroupGenerationIDs[groupID] == groupGeneration)
                    {
                        _GroupDirtyMask[index] |= mask;
                        _GroupsMaskDirty = true;
                    }
                }
            }
        }

        /// <summary>
        /// Clear the dirty flag set for given group
        /// Advanced users only
        /// </summary>
        /// <param name="groupID">Group ID of the group for which the flag should be cleared</param>
        private void UnmarkGroup_NoLock(UInt32 groupID)
        {
            lock (_Lock)
            {
                UInt64 index = (UInt64)groupID / 64;
                UInt64 mask = (UInt64)groupID & 63;
                _GroupDirtyMask[index] &= ~mask;
            }
        }

        /// <summary>
        /// Check if given group ID is marked as dirty
        /// </summary>
        /// <param name="groupID"></param>
        /// <returns></returns>
        private bool IsGroupMarked_NoLock(UInt64 groupID)
        {
            UInt64 index = groupID / 64;
            UInt64 mask = groupID & 63;
            return 0 != (_GroupDirtyMask[index] & mask);
        }

        /// <summary>
        /// Allocate new group ID for the group
        /// </summary>
        /// <returns></returns>
        private UInt32 AllocGroupID_NoLock()
        {
            // pop back
            if (_FreeGroupIndices.Count > 0)
            {
                var lastEntry = _FreeGroupIndices.Count - 1;
                var index = _FreeGroupIndices[lastEntry];
                _FreeGroupIndices.RemoveAt(lastEntry);
                return index;
            }

            // allocate new one
            UInt32 newIndex = _FirstFreeGroupIndex++;
            return newIndex;
        }

        /// <summary>
        /// Allocate single generation number
        /// </summary>
        private UInt32 AllocGroupGeneraion_NoLock()
        {
            return _GroupGenerationIDCounter++;
        }

        /// <summary>
        /// Access lock
        /// </summary>
        private readonly object _Lock;
        
        /// <summary>
        /// List of dirty state groups (by ID)
        /// </summary>
        private UInt64[] _GroupDirtyMask; // resized when needed

        /// <summary>
        /// Registered groups
        /// </summary>
        private Dictionary<UInt32, StateGroup> _Groups;

        /// <summary>
        /// GroupGenerationIDs for each valid group
        /// Comparing entry from this table with value in the incoming request allows to verify if the group is legit
        /// </summary>
        private UInt32[] _GroupGenerationIDs;

        /// <summary>
        /// Free group indices
        /// </summary>
        private List<UInt32> _FreeGroupIndices;

        /// <summary>
        /// Always increasing group generation counter
        /// </summary>
        private UInt32 _GroupGenerationIDCounter;

        /// <summary>
        /// First free index for group ID allocator
        /// </summary>
        private UInt32 _FirstFreeGroupIndex;

        /// has dirty groups ?
        private bool _GroupsMaskDirty;
    }
}
